Moje odkazy
Obsah článku:
vydáno: 11. 6. 2017 19:05, aktualizováno: 5. 7. 2020 16:39
Dnes oprášíme zase jednu starou dobrou technologii – paralelení port – a ukážeme si, jak ji softwarově ovládat. Protože je to dost nízkoúrovňová záležitost, nebude to tentokrát v Javě ale v C++. Cíl bude poměrně skromný: generovat obdélníkový signál s frekvencí 10 000 Hz a zadanou střídou (což zde neznamená střed chleba). Ve výsledku budu signál generovat jiným programem, nicméně nejdřív si chci otestovat jednotlivé části systému samostatně, takže teď to bude jen LEDka a pár řádků kódu bez nějakých složitostí.
Dříve býval paralelní port běžnou součástí každého počítače (pamatuji je od 386 po Pentia a Athlony), ovšem na dnešních základních deskách je najdeme jen sporadicky. Prodávají se ale dodatečné PCI a PCIe karty.
Port se používal hlavně pro připojení tiskáren (ostatně taky se mu říká LPT – Line Print Terminal) případně jiných periferií. Ale je to poměrně univerzální rozhraní – skoro jako GPIO, které známe třeba z Arduina nebo Raspberry Pi. Můžeme zde nastavovat logickou 0 a 1 resp. napětí 0 a 5 V na jednotlivých pinech a stejně tak je i číst. (5 V je teoretická maximální hodnota, ve skutečnosti bývá napětí nižší)
V základu máme k dispozici 8 + 4 výstupní a 5 vstupních pinů. U novějších portů máme obousměrné piny a můžeme mít více vstupů. Více na Wikipedii: Parallel port a v článku Interfacing the Standard Parallel Port:
The non bi-directional ports were manufactured with the 74LS374's output enable tied permanent low, thus the data port is always output only. When you read the Parallel Port's data register, the data comes from the 74LS374 which is also connected to the data pins. Now if you can overdrive the '374 you can effectively have a Bi-directional Port. (or a input only port, once you blow up the latches output!)
What is very concerning is that people have actually done this. I've seen one circuit, a scope connected to the Parallel Port distributed on the Internet. The author uses an ADC of some type, but finds the ADC requires transistors on each data line, to make it work! No wonder why. Others have had similar trouble, the 68HC11 cannot sink enough current (30 to 40mA!)
Pro běžné periferie se dnes prakticky nepoužívá, přesto má svoje nesporné výhody a dodnes najde využití. Na rozdíl od od Arduina nebo RPi máme k dispozici výkonný stroj (se spoustou paměti, diskem, rychlou sítí atd.) a na rozdíl od sériového nebo nedejbože USB portu máme spolehlivé nízkoúrovňové rozhraní s minimální (a stabilní) latencí.
Z paralelního portu můžeme udělat třeba zvukovou kartu, připojit přes něj gamepad od SNESu nebo jím ovládat nějaký stroj (což bude zajisté užitečnější).
S paralelním portem budeme pracovat nízkoúrovňově, takže místo souboru /dev/parport0
nás zajímá přímo adresa příslušného portu v paměti. Tu v GNU/Linuxu zjistíme následujícím příkazem:
# cat /proc/ioports | grep parport 0378-037a : parport0 037b-037f : parport0 e400-e402 : parport1 e403-e407 : parport1
První paralelní port, který je na základní desce, mívá typicky adresu 0x378
. Port na mojí přídavné kartě má adresu 0xe400
.
V zásadě jde jen o cyklus, který provede patřičný počet opakování a uvnitř nastaví hodnotu pinu, chvíli počká, nastaví opačnou hodnotu a zase chvíli počká. Program nemá žádné volby a je parametrizovaný přímo v kódu:
int addr = 0xe400; // parallel port address
int baseFreq = 10000; // base frequency in Hz
int outputPower = 10; // duty cycle; 100 = 100 %
int duration = 1; // in seconds; total sleep time
Program spustíme pod rootem pomocí příkazu make run
(pokud jsme kód změnili, před spuštěním se překompiluje, takže se s tím dá pracovat podobně, jako se skriptem). Zdrojové kódy jsou v mercurialu: lpt-signal-generator a níže v textu.
Než začneme pracovat s paralelním portem, musíme zavolat funkci ioperm()
:
if (ioperm(addr,1,1)) {
fwprintf(stderr, L"Access denied to port %#x\n", addr), exit(1);
}
Pokud nejsme root, tak program hned skončí. Trochu potíž je v tom, že když zadáme špatnou adresu, tak to projde – jednou se mi stalo, že PCI karta byla špatně zastrčená, takže ji systém neviděl – a program běžel, jako by nic, ale LEDka neblikala. Tohle by chtělo ještě vylepšit (našel jsem jen, jak to udělat přes ppdev
ovladač, open()
a ioctl()
, ale to není přímý přístup k portu).
A teď už se můžeme pustit do nastavování hodnot na pinech paralelního portu:
auto cycleCount = duration * baseFreq;
for (auto i = cycleCount; i > 0; i--) {
outb(0b00000001, addr);
usleep(timeOn);
outb(0b00000000, addr);
usleep(timeOff);
}
V C++14 (a v GNU GCC někdy od roku 2007) můžeme zapisovat binární čísla, takže je to hezky přehledné. Ta jednička na konci znamená, že na prvním datovém pinu (resp. pinu č. 2 na konektoru DB-25) nastavíme napětí 5 V, zatímco na ostatních datových pinech bude 0 V.
Funkce outb()
zapisuje celý bajt, takže nám přepíše i sedm zbývajících bitů. Pokud bychom chtěli zachovat jejich předchozí hodnoty, museli bychom si je někde zapamatovat. Práci s jednotlivými piny umožňuje knihovna Parpin.
Naším cílem je generovat obdélníkový signál se zadanou frekvencí (10 000 Hz) a zadaným poměrem časů, ve kterých je signál nahoře a dole – to se jmenuje Střída:
Abychom věděli, jak dlouho čekat ve stavu 1 a jak dlouho ve stavu 0, musíme jsme si spočítat dva časy:
// in microseconds:
auto oneSecond = 1000 * 1000;
auto timeOn = oneSecond * outputPower / 100 / baseFreq;
auto timeOff = oneSecond * (100 - outputPower) / 100 / baseFreq;
V zápětí ale zjistíme, že to moc nefunguje, resp. program běží víc než dvakrát déle, než by měl:
# time ./lpt-signal-generator Parallel port: e400 Base frequency: 10 000 Hz Output power: 10 % duty cycle Duration: 1 s Cycle count: 10 000 × Time on: 10 μs 1× in each cycle Time off: 90 μs 1× in each cycle single outb(): 64 μs 2× in each cycle single outb(): 64 335 ns 2× in each cycle real 0m2.292s user 0m0.044s sys 0m0.176s
Ačkoli frekvence 10 000 Hz nevypadá na první pohled až tak vysoká (takt procesorů je v řádech GHz), dali jsme systému pořádně zabrat. Pouštět to jako běžný proces nikam nevede. Náš program musíme pustit s prioritou reálného času (a nemusí to být nutně na systému s RT jádrem). To už je o hodně lepší:
# time chrt 1 ./lpt-signal-generator Parallel port: e400 Base frequency: 10 000 Hz Output power: 10 % duty cycle Duration: 1 s Cycle count: 10 000 × Time on: 10 μs 1× in each cycle Time off: 90 μs 1× in each cycle single outb(): 13 μs 2× in each cycle single outb(): 13 016 ns 2× in each cycle real 0m1.265s user 0m0.044s sys 0m0.144s
Ale pořád ne dost dobré. Původní implementace je totiž dost naivní a nepočítá s časem stráveným ve funkci outb()
. Takže jsem tam přidal výpočet, kolik μs se v těchto funkcích strávilo – k tomu jsem použil chrono::high_resolution_clock
z C++, kde se pracuje s přesností na nanosekundy. Dosud jsem se pohyboval někde o řád až dva výše – např. pokud SQL dotaz trval pod 1 ms, tak se to zaokrouhlilo na 0, byl to krásný výsledek a nebylo, co dál řešit. Tady najednou ale i jednotky μs jsou moc dlouho.
Nečekám, že by to šlo ještě nějak moc optimalizovat – přeci jen interakce s operačním systémem a hardwarem nějaký čas zabere – takže nezbývá než patřičně zkrátit časy čekání. Zatím nevíme, zda se na daném pinu objeví těch 5 V spíše na začátku těch 13 μs nebo spíše na konci (pravděpodobnější), ale to je celkem jedno, protože by to způsobilo jen posun a proporce časů mezi 0 a 1 by zůstaly stejné. Horší by bylo, pokud by se např. směrem nahoru na 5 V nastavovala hodnota rychleji než směrem dolů na 0 V. Ale to softwarově nezjistíme.
Nastal čas připojit logický analyzátor. Stejně jako při zkoumání SNESu jsem použil otevřený hardware DSLogic:
První měření – frekvence 1 000 Hz a střída 10 % – dopadlo relativně dobře, DSLogic ukazuje 947 Hz a 12,52 %:
Druhý pokus: mělo být 5 000 Hz a 10 % – naměřeno 4 230 Hz a 18,44 %:
Třetí pokus: mělo být 10 000 Hz a 10 % – naměřeno 6 580 Hz a 14,42 %:
První cykly bývají horší a kolísavé, pak se to zlepšuje, ale požadovaných hodnot náš generátor nikdy nedosáhne. Stabilizuje se na 7 850 Hz a 18,65 %. Na tuto stabilitu se ale nedá moc spolehnout a dost možná bude záviset na teplotě a měsíčním svitu. Možná by pomohlo zapálení obřadních svíček kolem počítače.
Teď by bylo na místě implementovat PID regulátor (PID = proporcionální, integrační a derivační; totéž, co máte ve své hexakoptéře/kvadkoptéře), jenže k tomu potřebujeme zpětnou vazbu a je to netriviální úloha nad rámec tohoto programu, takže se pokusím jen zkrátit časy čekání o čas strávený ve funkci outb()
.
Pro začátek zkusíme vidlácké programování a odečteme magické konstanty. Potřebujeme odečíst 2×13 μs, ovšem kdybychom odečetli 13 od 10, tak se dostaneme do mínusu (ne, usleep(-3)
opravdu necestuje zpět v čase), takže musíme být trochu kreativní:
timeOn -= 10;
timeOff -= 16;
Program nám teď vypisuje:
Parallel port: e400 Base frequency: 10 000 Hz Output power: 10 % duty cycle Duration: 1 s Cycle count: 10 000 × Time on: 0 μs 1× in each cycle Time off: 74 μs 1× in each cycle single outb(): 0 μs 2× in each cycle single outb(): -14 ns 2× in each cycle
Poslední dva řádky teď jsou nepravdivé – je třeba je číst tak, že těch 10 000 cyklů doběhlo s odchylkou -280 μs, takže celkem přesně. Čas strávený ve funkci outb()
je samozřejmě pořád stejný.
Na tomto HW je tento čas dlouhodobě stabilně těch 13 μs, takže už to dává docela hezké výsledky – frekvenci jsme už trefili (první cykly byly opět špatné, tohle jsou pozdější):
Nicméně střída je pořád mimo: 12,22 % místo požadovaných 10 %. Z toho plyne, že střída pod cca 13 % je na tomto HW a s touto frekvencí nedosažitelná, protože i když voláme obě outb()
funkce hned po sobě (timeOn = 0
), na 10 μs mezi přepnutími úrovní se nedostaneme. (další věc je, že nějaký čas zabere i dekrementování proměnné a vyhodnocování podmínky v cyklu – tyto časy program neprávem přičítá režii funkce outb()
, ale to asi moc nebude)
Zkusíme tedy něco lehčího – střídu 50 % – kde už můžeme odečíst 13 μs na obou stranách:
int outputPower = 50; // duty cycle; 50 = 50 %
timeOn -= 13;
timeOff -= 13;
A tohle už je výsledek, se kterým můžu být spokojený – 10 340 Hz a 49,95 % stabilně (první cykly jsem zase přeskočil):
10 000 Hz / 20 % → 9 920 Hz / 20,14 %:
10 000 Hz / 80 % → 10 260 Hz / 80,92 %:
Opět pěkné výsledky. Při opakovaných měřeních kolísají hodnoty oběma směry, takže bych z těch rozdílných odchylek u 20 a 80 % zatím nevyvozoval žádné závěry.
Výše uvedené řešení samozřejmě nebude fungovat na jiných počítačích, kde bude magická konstanta jiná. Proto do programu přidáme automatickou kalibraci – na začátku pustíme nějaké cykly naprázdno (zapisujeme samé nuly), abychom zjistili, jakou má daný systém režii, a podle toho určili potřebnou korekci časů čekání.
Program taky vyhodnotí, zda je zadaná frekvence a střída na daném hardwaru dosažitelná:
# make run chrt 1 ./lpt-signal-generator Parallel port: e400 Base frequency: 10 000 Hz Output power: 20 % duty cycle Duration: 1 s Cycle count: 10 000 × Time on: 20 μs 1× in each cycle Time off: 80 μs 1× in each cycle Single outb(): 13 μs 2× in each calibration cycle Single outb(): 13 743 ns 2× in each calibration cycle Minimum power: 13 % feasible duty cycle Maximum power: 87 % feasible duty cycle Calibration: OK both frequency and duty cycle should be correct Sleep on: 7 μs 1× in each cycle Sleep off: 67 μs 1× in each cycle Deviation: 15 680 μs in total Deviation: 1 568 ns in each cycle
Zvažoval jsem i průběžnou kalibraci během hlavního cyklu, ale tam by asi zdržovalo i samotné měření.
Makefile:
CXX=g++
CXXFLAGS += -std=c++11 -Wall
APP=lpt-signal-generator
PROGNAME=$(APP)
SRC=lpt.cpp
ALL: $(PROGNAME)
$(PROGNAME): $(SRC)
$(CXX) $(CXXFLAGS) -o $(PROGNAME) $(SRC)
clean:
rm -f $(PROGNAME)
run: $(PROGNAME)
chrt 1 ./$(PROGNAME)
Celý program lpt-signal-generator:
/**
* LPT signal generator
* Copyright © 2017 František Kučera (frantovo.cz)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdlib.h>
#include <iostream>
#include <stdio.h>
#include <math.h>
#include <sys/io.h>
#include <unistd.h>
#include <chrono> // requires -std=c++11
/**
* can not mix printf and wprintf
* see https://stackoverflow.com/questions/8681623/printf-and-wprintf-in-single-c-code
* > This is to be expected; your code is invoking undefined behavior.
* > Per the C standard, each FILE stream has associated with it an "orientation" (either "byte" or "wide)
* > which is set by the first operation performed on it, and which can be inspected with the fwide function.
* > Calling any function whose orientation conflicts with the orientation of the stream results in undefined behavior.
*/
#include <wchar.h>
#include <locale.h>
using namespace std;
// TODO: data types revision
// TODO: time units s, μs, ns – naming convention / unification
/**
* generates square wave signal on a parallel port pin with given frequency and duty cycle
* mode info: https://blog.frantovo.cz/c/358/Paraleln%C3%AD%20port%20jako%20gener%C3%A1tor%20sign%C3%A1lu
*/
int main() {
//cout << "LPT!" << endl; // same as using printf → breaks all folllowing wprintf() calls, see note above
/*
* if setlocale() is missing, unicode characters are replaced with ? or „→“ with „->“ because C/POSIX locale is used,
* see man setlocale:
* > On startup of the main program, the portable "C" locale is selected as default.
* > If locale is an empty string, "", each part of the locale that should be modified is set according to the environment variables.
*/
setlocale(LC_ALL,"");
// configuration ----
int addr = 0xe400; // parallel port address; first number of given port in: cat /proc/ioports | grep parport
int baseFreq = 10000; // base frequency in Hz, should be between 5 000 between 10 000 Hz; lower frequency leads to dashed/dotted lines instead of greyscale
int outputPower = 20; // duty cycle; 100 = 100 %
int duration = 1; // in seconds; total sleep time, see note above
// ------------------
int valueWidth = 10; // just for padding of printed values
int labelWidth = -15; // just for padding of printed labels
// ' = thousand separator
// * = padding
wprintf(L"%*ls %*x\n", labelWidth, L"Parallel port:", valueWidth, addr); // or %#*x – adds 0x prefix
wprintf(L"%*ls %'*d Hz\n", labelWidth, L"Base frequency:", valueWidth, baseFreq);
wprintf(L"%*ls %*d %% duty cycle\n", labelWidth, L"Output power:", valueWidth, outputPower);
wprintf(L"%*ls %'*d s\n", labelWidth, L"Duration:", valueWidth, duration);
// in microseconds:
auto oneSecond = 1000 * 1000;
auto timeOn = oneSecond * outputPower / 100 / baseFreq;
auto timeOff = oneSecond * (100 - outputPower) / 100 / baseFreq;
auto cycleCount = duration * baseFreq;
wprintf(L"%*ls %'*d ×\n", labelWidth, L"Cycle count:", valueWidth, cycleCount);
wprintf(L"%*ls %'*d μs 1× in each cycle\n", labelWidth, L"Time on:", valueWidth, timeOn);
wprintf(L"%*ls %'*d μs 1× in each cycle\n", labelWidth, L"Time off:", valueWidth, timeOff);
//wprintf(L"%*ls %*ls\n", labelWidth, L"unicode test:", valueWidth, L"čeština → …");
wprintf(L"\n");
// TODO: test whether this address is an parallel port
if (ioperm(addr,1,1)) { fwprintf(stderr, L"Access denied to port %#x\n", addr), exit(1); }
// calibration
auto startTimestamp = chrono::high_resolution_clock::now();
auto calibrationCycles = 10000;
auto calibrationSleepTime = 10;
for (auto i = calibrationCycles; i > 0; i--) {
outb(0b00000000, addr);
usleep(calibrationSleepTime);
outb(0b00000000, addr);
usleep(calibrationSleepTime);
}
auto finishTimestamp = chrono::high_resolution_clock::now();
auto measuredDuration = chrono::duration_cast<chrono::nanoseconds>(finishTimestamp - startTimestamp).count();
auto singleOutbCostNano = (measuredDuration - calibrationCycles*2*calibrationSleepTime*1000)/calibrationCycles/2;
auto singleOutbCostMicro = singleOutbCostNano/1000;
wprintf(L"%*ls %'*d μs 2× in each calibration cycle\n", labelWidth, L"Single outb():", valueWidth, singleOutbCostMicro);
wprintf(L"%*ls %'*d ns 2× in each calibration cycle\n", labelWidth, L"Single outb():", valueWidth, singleOutbCostNano);
auto minPower = 100*singleOutbCostNano/(1000*1000*1000/baseFreq);
auto maxPower = 100-minPower;
wprintf(L"%*ls %*d %% feasible duty cycle\n", labelWidth, L"Minimum power:", valueWidth, minPower);
wprintf(L"%*ls %*d %% feasible duty cycle\n", labelWidth, L"Maximum power:", valueWidth, maxPower);
if (singleOutbCostMicro < timeOn && singleOutbCostMicro < timeOff) {
wprintf(L"%*ls %*ls both frequency and duty cycle should be correct\n", labelWidth, L"Calibration:", valueWidth, L"OK");
timeOn -= singleOutbCostMicro;
timeOff -= singleOutbCostMicro;
} else if (2*singleOutbCostMicro < (timeOn + timeOff)) {
wprintf(L"%*ls %*ls frequency should be OK, but duty cycle is not feasible\n", labelWidth, L"Calibration:", valueWidth, L"WARNING");
timeOn -= singleOutbCostMicro;
timeOff -= singleOutbCostMicro;
if (timeOn < 0) {
timeOff -= timeOn;
timeOn = 0;
} else {
timeOn -= timeOff;
timeOff = 0;
}
} else {
wprintf(L"%*ls %*ls both frequency and duty cycle are not feasible\n", labelWidth, L"Calibration:", valueWidth, L"ERROR");
timeOn = 0;
timeOff = 0;
}
wprintf(L"%*ls %'*d μs 1× in each cycle\n", labelWidth, L"Sleep on:", valueWidth, timeOn);
wprintf(L"%*ls %'*d μs 1× in each cycle\n", labelWidth, L"Sleep off:", valueWidth, timeOff);
wprintf(L"\n");
// actual signal generation
startTimestamp = chrono::high_resolution_clock::now();
for (auto i = cycleCount; i > 0; i--) {
outb(0b00000001, addr); // first data out pin = data out 0 = pin 2 on DB-25 connector
usleep(timeOn);
outb(0b00000000, addr);
usleep(timeOff);
}
finishTimestamp = chrono::high_resolution_clock::now();
measuredDuration = chrono::duration_cast<chrono::nanoseconds>(finishTimestamp - startTimestamp).count();
wprintf(L"%*ls %'*d μs in total\n", labelWidth, L"Deviation:", valueWidth, (measuredDuration-duration*oneSecond*1000)/1000);
wprintf(L"%*ls %'*d ns in each cycle\n", labelWidth, L"Deviation:", valueWidth, (measuredDuration-duration*oneSecond*1000)/cycleCount);
}
Statistika počtu řádků kódu pomocí příkazu CLOC:
$ cloc-sql.sh . ╭────────┬─────────┬───────────┬───────────┬──────┬────────┬──────────────────╮ │ jazyk │ souborů │ prázdných │ komentářů │ kódu │ celkem │ celkem_graf │ ├────────┼─────────┼───────────┼───────────┼──────┼────────┼──────────────────┤ │ C++ │ 1 │ 34 │ 47 │ 85 │ 166 │ ████████████████ │ │ make │ 1 │ 6 │ 0 │ 12 │ 18 │ ██░░░░░░░░░░░░░░ │ │ celkem │ 2 │ 40 │ 47 │ 97 │ 184 │ │ ╰────────┴─────────┴───────────┴───────────┴──────┴────────┴──────────────────╯ Record count: 3
Pomocí osciloskopu jsem ověřil, že generovaný signál je skutečně obdélníkový. Frekvence je 9 930 Hz a střída měla být v tomto případě 30 %, což tak nějak odpovídá. Ovšem napětí je jen cca 2,8 V, nikoli 5 V.
Ani na paralelním portu, který je přímo na desce, jsem nenaměřil 5 V – bylo tam nějakých 2,95 V. Specifikaci to ale neodporuje, protože paralelní port by měl mít TTL high od +2,4 do +5 V a TTL low od 0 do +0,8 V. Pro kontrolu: na USB portu jsem 5 V naměřil.
K naměřeným hodnotám by se slušelo uvést konfiguraci. Protože jsem potřeboval druhý paralelní port, bylo nutné přidat PCI kartu. Jako první jsem zkoušel kartu s čipem MosChip MCS 9835 CV:
ale s tou jsem neuspěl – systém ji sice viděl, ovšem LEDka neblikala. Údajně je dobré se vyhnout čipům 9805/9815 a použít raději 9845/9865/9901. Zkusím to ještě rozchodit, ale prozatím jsem použil jinou kartu s čipem MosChip MCS 9865 1V, která fungovala hned:
Celé to běží na starším Celeronu a desce Asus P5PE-VM, ale dokud to bude fungovat, není důvod HW měnit.
# cat /proc/cpuinfo processor : 0 vendor_id : GenuineIntel cpu family : 15 model : 4 model name : Intel(R) Celeron(R) CPU 2.80GHz stepping : 9 microcode : 0x3 cpu MHz : 2793.026 cache size : 256 KB fdiv_bug : no hlt_bug : no f00f_bug : no coma_bug : no fpu : yes fpu_exception : yes cpuid level : 5 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe lm constant_tsc up pebs bts pni dtes64 monitor ds_cpl tm2 cid cx16 xtpr lahf_lm bogomips : 5586.05 clflush size : 64 cache_alignment : 128 address sizes : 36 bits physical, 48 bits virtual power management:
Verze jádra:
# uname -ro 3.4-9-rtai-686-pae GNU/Linux
Tohle přímo nesouvisí s řešenou úlohou, ale mně prostě vadí, když některé základní věci nefungují, tak mi to nedalo a aspoň jsem se naučil, jaké funkce používat pro vypisování i jiných než ASCII znaků.
V čem je problém? Dnes se používá převážně vícebajtové kódování (většinou UTF-8), takže neplatí, že 1 znak = 1 bajt, a kvůli tomu nefunguje spousta starých funkcí, které vznikly v době, kdy tento předpoklad platil. Vícebajtové znaky se nám pomocí nich sice nějak vypsat podaří, ale nebudou fungovat takové věci jako počítání šířky řetězce, což je potřeba, když chceme např. doplnit patřičný počet mezer zleva nebo zprava pro zarovnání.
Místo klasické funkce printf()
:
int vw = 10; // valueWidth: just for padding of printed values
int lw = -15; // labelWidth: just for padding of printed labels
printf("%*s %'*d μs 1× in each cycle\n", lw, "Time off:", vw, timeOff);
tedy potřebujeme:
#include <wchar.h>
#include <locale.h>
…
wprintf(L"%*ls %'*d μs 1× in each cycle\n", lw, L"Time off:", vw, timeOff);
printf()
a wprintf()
První záludnost, na kterou jsem narazil, je to, že v kódu nemůžeme míchat funkce printf()
a wprintf()
– musíme si jednu vybrat a té se držet – jinak nám totiž ta druhá skupina funkcí nebude fungovat (záleží, kterou v kódu zavoláme jako první – pěkný vedlejší efekt). Je to popsané v dokumentaci:
It is important to never mix the use of wide and not wide operations on a stream. There are no diagnostics issued. The application behavior will simply be strange or the application will simply crash. The
fwide
function can help avoid this.
Nevypíše se žádná chyba – ani při kompilaci ani v době běhu – druhý typ funkcí se prostě rozbije a nebude vypisovat nic. K rozbití wprintf()
funkcí vede i nevinně vypadající řádek:
cout << "LPT!" << endl;
setlocale()
Když už vše přepíšeme na wprintf()
, a přestaneme tak míchat dohromady bajtové a znakové funkce, a změníme "…"
na L"…"
a %s
na %ls
, tak zjistíme, že to stejně nefunguje a místo znaků s háčky a čárkami to píše otazníky a místo např. →
to píše ->
. Program totiž používá C/POSIX lokalizaci a tyto znaky nepodporuje. Napravíme to zavoláním funkce:
setlocale(LC_ALL,"");
Z manuálové stránky:
On startup of the main program, the portable "C" locale is selected as default.
If locale is an empty string, "", each part of the locale that should be modified is set according to the environment variables.
Většinou používám Netbeans a to i pro jiné jazyky než Javu. Ale tentokrát jsem si IDE sestavil z jednodušších nástrojů a kód psal a spouštěl (přes SSH) rovnou na vzdáleném stroji, kde mám paralelní port.
Jako editor jsem použil Emacs, obrazovku rozdělil vertikálně na dvě poloviny pomocí Screen a kompiloval přes Make v GCC (všechno GNU nástroje).
Rychlokurz GNU Screen:
Zatím máme sice jen blikající LEDku, ale zato je to blikání řízené CPU a poměrně přesně časované. Co se týče kódu, bylo to hlavně programátorské cvičení a výlet někam jinam – do světa μs a jiného jazyka, než v kterém běžně pracuji, což občas neškodí.
P.S. nejsem C ani C++ programátor, takže mě rozhodně neurazí, když budete mít nějaké připomínky k mému kódu a návrhy na vylepšení.
Cyklické spouštění nějaké úlohy s danou periodou je úplně základní use case v realtime aplikacích. Přesně na tyhle úlohy se místo usleep() používá clock_nanosleep(), který řeší právě probuzení v daném absolutním čase. Viz příklad https://rt.wiki.kernel.org/index.php/Squarewave-example -- přesně stejné použítí paralelního portu
Odpadají pak všechny potíže, kterými se zabývá většina obsahu článku. Nicméně přesnost časů pod 100 us je na PC/Linux platformě už docela problém, což nakonec ukazují i výsledky v článku. Viz také nástroj cyclictest. Výrazně lepších výsledků by se dalo dosáhnout implementací v kernel space jako modul, ale pořád to bude použitelné více méně jen na hraní.
Předpokládám, že to neběželo na kernelu s RT PREEMPT patchem. Pak by bylo zajímavé si třeba na tom osciloskopu udělat analýzu min/max periody. Průměr bude v pořádku, ale téměř jistě tam budou místy případy, kdy to bude úplně mimo. Zvlášť, pokud paralelně s tím programem poběží nějaká další zátěž. Záleží pak na aplikaci, jestli to lze tolerovat nebo ne.